# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

try:
    import json
except ImportError:
    import simplejson as json

import httplib2
import urllib2
import io
import re
import gzip
import os
import csv
import shutil
import zipfile
import sqlite3
from datetime import datetime
import StringIO
from io import BytesIO
import neuro.db as db
from lxml import etree
from neuro.common import isInteger, isFloat, isString
import neuro.arrays as arrays
import neuro.filesystem as filesystem
import neuro.system as system
import neuro.strings as strings
from neuro.base import Object
from neuro.models.xml import XML
from neuro.exceptions import BaseException
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import AuthenticationException
from neuro.exceptions import FileNotFoundException
from neuro.exceptions import IOException
from neuro.net.http import HttpRequest
from neuro.net.http import HttpRequestException
from neuro.command import Command

class XnatConfig(Object):
    '''
    XNAT configuration object
    '''
    def __init__(self, filename="~/.xnat_auth", autoload=True):
        '''
        Constructor
        
        Example configuration file ::
            
            <xnat>
                <gspcentral version="1.5">
                    <url>http://gspcentral.dipr.partners.org</url>
                    <username>jdoe</username>
                    <password>p@$$w0rd</password>
                </gspcentral>
                <cbscentral version="1.5">
                    <url>https://cbscentral.rc.fas.harvard.edu</url>
                    <username>jdoe</username>
                    <password>p@$$w0rd</password>
                </cbscentral>
            </xnat>
        
        Example Usage ::
        
            from neuro.apps.xnat import XnatConfig
            
            config = XnatConfig()
            config = XnatConfig("gspcentral")
        
        :param filename: Configuration file name
        :type filename: str
        :param autoload: Automatically load configuration file
        :type autoload: bool
        '''
        if(not isinstance(filename, basestring)):
            raise IllegalArgumentException("Filename must be an instance of str")

        self._filename = filename
        self._config = None

        if(autoload):
            self.load()

    def load(self):
        '''
        Load/reload configuration file
        '''
        self._filename = filesystem.canonical(self._filename)
        self._config = XML(self._filename)

    def byAlias(self, alias):
        '''
        Get credentials and connection information by configuration file alias ::
            
            >>> config.byAlias("gspcentral")
            {'url': 'http://gspcentral.dipr.partners.org', 'username': 'jdoe', 'password': 'p@$$w0rd', 'version': '1.5'}

        :param alias: Configuration file alias
        :type alias: str
        :returns: Credentials and connection information
        :rtype: dict
        :raises: :class:`ConfigException`
        '''
        if(not isinstance(alias, basestring)):
            raise IllegalArgumentException("Alias must be an instance of str")

        alias = alias.strip()

        if(alias == ""):
            raise IllegalArgumentException("Alias cannot be empty")

        if(self._config == None):
            raise ConfigException(ConfigException.NOT_LOADED, self._filename, alias)

        deployment = self._config.xpath("/xnat/" + alias)

        if(deployment == None):
            raise ConfigException(ConfigException.MISSING_DEP, self._filename, alias)

        url = self._config.xpath("/xnat/" + alias + "/url/text()")
        version = self._config.xpath("/xnat/" + alias + "/@version")
        username = self._config.xpath("/xnat/" + alias + "/username/text()")
        password = self._config.xpath("/xnat/" + alias + "/password/text()")

        ## --- validate the bejesus out of these values
        if(not isinstance(url, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, alias, "url")
        elif(not isinstance(version, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, alias, "version")
        elif(not isinstance(username, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, alias, "username")
        elif(not isinstance(password, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, alias, "password")

        url = url.strip()
        version = version.strip()
        username = username.strip()
        password = password.strip()

        if(url == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "url")
        elif(version == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, version, "version")
        elif(username == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "username")
        elif(password == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "password")

        return {"url": url, "version": version, "username": username, "password": password}

    def byURL(self, url):
        '''
        Get credentials and connection information by URL ::
        
            >>> config.byURL("http://gspcentral.dipr.partners.org")
            {'url': 'http://gspcentral.dipr.partners.org', 'username': 'jdoe', 'password': 'p@$$w0rd', 'version': '1.5'}

        :param url: Configuration file URL
        :type url: str
        :returns: Credentials and connection information
        :rtype: dict
        :raises: :class:`ConfigException`
        '''
        if(not isinstance(url, basestring)):
            raise IllegalArgumentException("URL must be an instance of str")

        url = url.strip()

        if(url == ""):
            raise IllegalArgumentException("URL cannot be empty")

        if(self._config == None):
            raise ConfigException(ConfigException.NOT_LOADED, self._filename, url)

        deployment = self._config.xpath("/xnat/*[url='" + url + "']")

        if(deployment == None):
            raise ConfigException(ConfigException.MISSING_DEP, self._filename, url)

        url = self._config.xpath("/xnat/*[url='" + url + "']/url/text()")
        version = self._config.xpath("/xnat/*[url='" + url + "']/@version")
        username = self._config.xpath("/xnat/*[url='" + url + "']/username/text()")
        password = self._config.xpath("/xnat/*[url='" + url + "']/password/text()")

        ## --- validate the bejesus out of these values
        if(not isinstance(url, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, url, "url")
        elif(not isinstance(version, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, version, "version")
        elif(not isinstance(username, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, url, "username")
        elif(not isinstance(password, basestring)):
            raise ConfigException(ConfigException.MISSING_OPT, self._filename, url, "password")

        url = url.strip()
        version = version.strip()
        username = username.strip()
        password = password.strip()

        if(url == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "url")
        elif(version == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "version")
        elif(username == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "username")
        elif(password == ""):
            raise ConfigException(ConfigException.BLANK_OPT, self._filename, alias, "password")

        return {"url": url, "version": version, "username": username, "password": password}

class Xnat(Object):
    '''
    Xnat base class
    '''
    MRSESSION = 0
    SUBJECT = 1
    GET = 3
    POST = 4
    PUT = 5
    DELETE = 6
    _request = None
    
    def __init__(self, input, timeout=30):
        '''
        Constructor ::
        
            from neuro.apps.xnat import Xnat
            
            xnat = Xnat.create("gspcentral")
            xnat = Xnat({"url": "http://gspcentral.dipr.partners.org", "username": "jdoe", "password": "p@$$w0rd"})

        :param input: Configuration alias or configuration dict
        :type input: str, dict
        :param timeout: Default timeout
        :type input: int
        '''
        self._deployment = None
        self._username = None
        self._password = None
        self._url = None
        self._config = None
        self._request = None
        self._timeout = timeout
        self._schemas = {}
        
        if(isinstance(input, basestring)):
            self.loadConfig(input)
        elif(isinstance(input, dict)):
            self.loadDict(input)
        else:
            raise IllegalArgumentException("Input must be an instance of str or dict")
    
    def loadConfig(self, alias, filename="~/.xnat_auth"):
        '''
        Load configuration information using a configuration file alias ::
        
            >>> xnat.loadConfig("gspcentral")

        :param alias: Configuration file alias 
        :type alias: str
        :param filename: Configuration file
        :type filename: str
        :raises: :class:`~neuro.exceptions.IOException`, 
                 :class:`~neuro.exceptions.FileNotFoundException`
                 :class:`ConfigException`
        '''
        if(not isinstance(alias, basestring)):
            raise IllegalArgumentException("Alias must be an instance of str")

        filename = filesystem.canonical(filename)
        alias = alias.strip()

        if(alias == ""):
            raise IllegalArgumentException("Alias cannot be empty")

        ## --- load config file into a XnatConfig object
        self._config = XnatConfig()
        info = self._config.byAlias(alias)

        ## --- store credential and connection information in instance variables
        self._url = info["url"]
        self._version = info["version"]
        self._username = info["username"]
        self._password = info["password"]

        if ("timeout" in info):
            self._timeout = info['timeout'];

    def loadDict(self, dictionary):
        '''
        Load connection information from dict ::
            
            >>> xnat.loadDict({"url": "http://gspcentral.dipr.partners.org", "username": "jdoe", "password": "p@$$w0rd"})
        
        :param dictionary: Connection information dictionary 
        :type dictionary: dict
        '''
        if(not isinstance(dictionary, dict)):
            raise IllegalArgumentException("Input dictionary must be an instance of dict")
        elif(len(dictionary) != 3):
            raise IllegalArgumentException("Expecting at least 3 elements in dictionary")

        if("url" not in dictionary):
            raise IllegalArgumentException("Dictionary must contain a \"url\" key (case-sensitive)")
        elif("username" not in dictionary):
            raise IllegalArgumentException("Dictionary must contain a \"username\" key (case-sensitive)")
        elif("password" not in dictionary):
            raise IllegalArgumentException("Dictionary must contain a \"password\" key (case-sensitive)")

        url = dictionary["url"]
        username = dictionary["username"]
        password = dictionary["password"]

        ## --- validate the bejesus out of these values
        if(not isinstance(url, basestring)):
            raise IllegalArgumentException("Value of \"url\" key must be an instance of str")
        elif(not isinstance(username, basestring)):
            raise IllegalArgumentException("Value of \"username\" key must be an instance of str")
        elif(not isinstance(password, basestring)):
            raise IllegalArgumentException("Value of \"password\" key must be an instance of str")

        url = url.strip()
        username = username.strip()
        password = password.strip()

        if(url == ""):
            raise IllegalArgumentException("Value of \"url\" key cannot be empty")
        elif(username == ""):
            raise IllegalArgumentException("Value of \"username\" key cannot be empty")
        elif(password == ""):
            raise IllegalArgumentException("Value of \"password\" key cannot be empty")

        self._url = url
        self._username = username
        self._password = password

    def getRequest(self):
        '''
        Get HttpRequest object ::
        
            >>> xnat.getRequest()
            <neuro.net.http.HttpRequest object at 0x7f93b5713b10>
        
        :returns: Raw :class:`~neuro.net.http.HttpRequest` object
        :rtype: :class:`~neuro.net.http.HttpRequest`
        '''
        if(self._request == None):
            self.connect()

        return self._request

    def getPath(self, path):
        '''
        Get the response body of URL path ::
        
            >>> xnat.getPath()
            '<HTML>\\n<HEAD>\\n ...'
        
        :param path: URL path
        :type path: str
        :returns: HTTP response body
        :rtype: str
        :raises: :class:`~neuro.net.http.HttpRequestException`
        '''
        request = self.getRequest()

        if(not isinstance(path, basestring)):
            raise IllegalArgumentException("path must be an instance of str")

        request.setPath(path)
        response = request.send(socktimeout=self._timeout)

        if(response.getStatus() != 200):
            raise HttpRequestException(HttpRequestException.RESPONSE_STATUS, request)

        return response.getBody().strip()

    def setURL(self, url):
        '''
        Set URL

        :param url: URL
        :type url: str
        '''
        if(not isinstance(url, basestring)):
            raise IllegalArgumentException("URL must be an instance of str")

        url = url.strip()

        if(url == ""):
            raise IllegalArgumentException("URL cannot be empty")

        self._url = url

    def getURL(self):
        '''
        Get loaded URL ::
        
            >>> xnat.getURL()
            'http://gspcentral.dipr.partners.org'
        
        :returns: XNAT URL
        :rtype: str
        '''
        return self._url

    def setUsername(self, username):
        '''
        Set XNAT username
        
        :param username: Username
        :type param: str
        '''
        if(not isinstance(username, basestring)):
            raise IllegalArgumentException("Username must be an instance of str")

        username = username.strip()

        if(username == ""):
            raise IllegalArgumentException("Username cannot be empty")

        self._username = username

    def getUsername(self):
        '''
        Get loaded username ::
        
            >>> xnat.getUsername()
            'jdoe'
        
        :returns: XNAT username
        :rtype: str
        '''
        return self._username

    def setPassword(self, password):
        '''
        Set XNAT password
        
        :param password: Password
        :type password: str
        '''
        if(not isinstance(password, basestring)):
            raise IllegalArgumentException("Password must be an instance of str")

        password = password.strip()

        ## --- due to principal
        if(password == ""):
            raise IllegalArgumentException("Password cannot be null")

        self._password = password

    def getPassword(self):
        '''
        Get loaded password. You should never use this method ::
        
            >>> xnat.getPassword()
            'p@$$w0rd'
        
        :returns: XNAT password
        :rtype: str
        '''
        return self._password
    
    def getStringAttributes(self, datatype, document_path="schemas/xnat/xnat.xsd"):
        '''
        Get all XNAT datatype schema 'xs:string' attributes ::
        
            >>> xnat.getStringAttributes(datatype="imageScanData")
            ['image_session_ID', 'note', 'quality', 'condition', 'series_description', 'modality', 'ID', 'type', 'UID', 'project']
            
        :param datatype:
        :type datatype: str
        :param document_path:
        :type document_path: str
        :rtype: lxml.etree
        '''
        if(not datatype):
            raise IllegalArgumentException("Datatype cannot be empty")
        
        if(document_path not in self._schemas):
            self._schemas[document_path] = self.getPath(document_path)
        
        schema = self._schemas[document_path]
        
        xml = XML(schema).toXML()

        result = xml.xpath("//xs:complexType[@name='" + datatype + "']/xs:attribute[@type='xs:string']/@name | " + \
                           "//xs:complexType[@name='" + datatype + "']/xs:sequence/xs:element[@type='xs:string']/@name",
                           namespaces=xml.nsmap)
        
        return result
    
    def api(self, path):
        '''
        Send request to XNAT light API ::
        
            >>> xnat.api("MrSession/id?label=AB1234C")
            {"values":["GSPCentral_E00001"]}
          
        :param path: API path
        :type path: str
        :returns: HTTP response body
        :rtype: str
        '''
        if(not isinstance(path, basestring)):
            raise IllegalArgumentException("API path must be an instance of str")

        if(path.strip() == ""):
            raise IllegalArgumentException("API path cannot be empty")

        request = HttpRequest(self.getURL() + "/api/" + path)
        request.setCredentials(self._username, self._password)
        response = request.send(socktimeout=self._timeout)

        return response.getBody().strip()

    def getAccessionIDs(self, label, type):
        '''
        Get Accession IDs given label. Uses XNAT light ::
        
            >>> xnat.getAccessionIDs("AB1234C", Xnat.MRSESSION)
            [u'GSPCentral_E00001']

        :param label: XNAT data label
        :type label: str
        :param type: Xnat.MRSESSION, Xnat.SUBJECT
        :type type: int
        :returns: List of Accession IDs
        :rtype: list
        :raises: :class:`XnatApiException`
        '''
        if(not isinstance(label, basestring)):
            raise IllegalArgumentException("Label must be an instance of str")
        elif(not isinstance(type, int)):
            raise IllegalArgumentException("Type must be an instance of int")
        elif(type != Xnat.MRSESSION and type != Xnat.SUBJECT):
            raise IllegalArgumentException("Type must be Xnat.MRSESSION or Xnat.SUBJECT")

        call = ""
        if(type == self.MRSESSION):
            call = "MrSession/id?label=" + label
        elif(type == self.SUBJECT):
            call = "Subject/id?label=" + label

        jsonstr = self.api(call)

        try:
            obj = json.loads(jsonstr)
        except Exception, e:
            raise XnatApiException(XnatApiException.RESPONSE, self, call, jsonstr)

        if("error" in obj):
            raise XnatApiException(XnatApiException.JSON_ERROR, self, call, json.dumps(obj))
        elif("values" not in obj):
            raise XnatApiException(XnatApiException.NO_VALUES, self, call, json.dumps(obj))
        elif(len(obj["values"]) == 0):
            raise XnatApiException(XnatApiException.EMPTY_VALUES, self, call, json.dumps(obj))

        ids = obj["values"]

        for i in range(len(ids)):
            if(ids[i].strip() == ""):
                raise XnatApiException(XnatApiException.BLANK_VALUE, obj)

            ids[i] = ids[i].strip()

        return ids

    def connect(self):
        '''
        Connect to XNAT. You should never need to use this method directly.
        
            >>> xnat.connect()
        
        :raises: :class:`~neuro.exceptions.AuthenticationException`
        '''
        if(Xnat._request):
            self._request = Xnat._request
            return

        self._request = HttpRequest(self._url)
        Xnat._request = self._request

        self._request.addPostParam("username", self._username)
        self._request.addPostParam("password", self._password)
        self._request.setPath("/app/action/XDATLoginUser")

        response = self._request.send(socktimeout=self._timeout)

        validation = strings.regex('.*<DIV class="error">.*', response.getBody());

        if(validation):
            raise AuthenticationException("URL: " + self._url + ", Username = " + self._username)

    @staticmethod
    def create(alias=None, url=None, timeout=30):
        '''
        Factory method for creating Xnat instances given a configuration file alias or Xnat URL ::
            
            >>> Xnat.create("gspcentral")
            <neuro.apps.xnat.Xnat_15 object at 0x1621ad0>
            >>> Xnat.create("http://nccentry.rc.fas.harvard.edu:9127")
            <neuro.apps.xnat.Xnat_15 object at 0x1621ad0>
            
        :param alias: Configuration file alias 
        :type alias: str
        :param url: Xnat URL
        :type url: str
        :returns: Xnat instance
        :rtype: :class:`Xnat`
        '''
        config = XnatConfig()
        
        if alias and url:
            raise IllegalArgumentException("You must specify either url or alias")        
        elif not alias and not url:
            raise IllegalArgumentException("You must specify either url or alias")
        
        if alias:
            info = config.byAlias(alias)
            input = alias
        elif url:
            info = config.byURL(url)
            input = {"url": info["url"], "username": info["username"], "password": info["password"]}
                    
        if(info["version"] == "1.4"):
            return Xnat_14(input, timeout=timeout)
        elif(info["version"] == "1.5"):
            return Xnat_15(input, timeout=timeout)

    @staticmethod
    def overviewToXML(overview):
        '''
        Convert overview table to XML :: 
        
            >>> ov = xnat.getOverview(format="csv")
            >>> xml_ov = xnat.overviewToXML(ov)

        :param overview: CSV formatted saved search results
        :type overview: str, list
        :returns: XML formatted saved search results
        :rtype: :class:`~neuro.xml.XML`
        '''
        if(isinstance(overview, basestring)):
            overview = Xnat.overviewToList(overview)
        elif(isinstance(overview, dict)):
            overview = overview.values()
        elif(not isinstance(overview, list)):
            raise IllegalArgumentException("Overview must be a string, list, or dict")

        if(overview == None or len(overview) == 0):
            return None

        headers = overview[0].keys()

        xml = "<overview>"

        for row in overview:
            xml += "<session id=\"" + row["Label"] + "\">"

            for header in headers:
                cleanTag = header.replace("/", "_").lower()
                xml += "<" + cleanTag + ">" + row[header] + "</" + cleanTag + ">"

            xml += "</session>"

        xml += "</overview>"

        return XML(xml)

    @staticmethod
    def overviewToDict(overview):
        '''
        Convert overview to a dict of dicts ::
        
            >>> ov = xnat.getOverview(format="csv")
            >>> ov_dict = xnat.overviewToDict(ov)

        :param overview: CSV formatted saved search results
        :type overview: str
        :returns: Dictionary formatted saved search results
        :rtype: dict
        '''
        if(not isinstance(overview, basestring)):
            raise IllegalArgumentException("Overview CSV must be an instance of str")
        elif(overview == ""):
            raise IllegalArgumentException("Overview CSV cannot be null")

        ## --- explode the array and move headers onto a separate list
        array = strings.toArray(overview)
        headers = array[0]
        array.pop(0)

        ## --- case that we only have table headers
        if(len(array) == 0):
            return None

        result = {}

        for row in array:
            new = dict(zip(headers, row))
            result[new["Label"]] = new

        return result
    
    def savedSearchDatabase(self, name, db_conn=None, date_formats={}):
        '''
        Convert saved search to a SQL database ::
        
            >>> conn = xnat.savedSearchDatabase(name="overview", date_formats={'Date': '%Y-%m-%d'})
            >>> conn = xnat.savedSearchDatabase(name="morphometrics", db_conn=conn)
            >>> cursor = conn.cursor()
            >>> cursor.execute("SELECT overview.Label,L_Amy_Vol FROM overview,morphometrics WHERE overview.Label=morphometrics.Label")
            >>> for row in cursor:
            ...     print(row)
            (u'AB1234C', 1740.0)
            (u'DE4567F', 1855.0)
            
        :param name: Saved search name
        :type name: str
        :param db_conn: Use existing database connection
        :type db_conn: sqlite3.Connection
        :param date_formats: Date field formatting
        :type date_formats: dict
        :rtype: sqlite3.Connection
        '''
        overview = self.getOverview(version=name, format="csv")
        
        if not isinstance(name, basestring):
            raise IllegalArgumentException("Name must be an instance of str")
        elif not isinstance(date_formats, dict):
            raise IllegalArgumentException("Date formats must be a dictionary")
        
        self.debug("Converting to database")
        return db.csvToDatabase(overview, table_name=name.lower(), db_conn=db_conn, date_formats=date_formats)
    
    @staticmethod
    def overviewToList(overview):
        '''
        Convert saved search results to a list of dicts ::
        
            >>> ov = xnat.getOverview(format="csv")
            >>> ov_list = xnat.overviewToList(ov)

        :param overview: CSV formatted saved search results
        :type overview: str, dict
        :returns: List of dicts formatted saved search results
        :rtype: list
        '''
        if(isinstance(overview, dict)):
            return overview.values()
        elif(not isinstance(overview, basestring)):
            raise IllegalArgumentException("Overview must be an instance of str (CSV)")

        overview = overview.strip()

        if(overview == ""):
            raise IllegalArgumentException("Overview CSV cannot be null")

        ## --- explode the array and move headers onto a separate list
        array = strings.toArray(overview)
        headers = array[0]
        array.pop(0)

        ## --- presumably we only have headers
        if(len(array) == 0):
            return None

        result = []

        for row in array:
            new = dict(zip(headers, row))
            result.append(new)

        return result

    @staticmethod
    def overviewToArray(overview):
        '''
        Convert saved search CSV results to a list of lists :: 
        
            >>> ov = xnat.getOverview(format="csv")
            >>> ov_array = xnat.overviewToArray(ov)

        :param overview: CSV formatted saved search results
        :type overview: str
        :returns: List of lists saved search results
        :rtype: list
        '''
        if(not isinstance(overview, basestring)):
            raise IllegalArgumentException("Overview must be an instance of str (CSV)")

        overview = overview.strip()

        if(overview == ""):
            raise IllegalArgumentException("Overview CSV cannot be null")

        array = strings.toArray(overview)

        ## --- presumably we only have headers
        if(len(array) <= 1):
            return None;

        return array
    
    def storeXML(self, filename):
        '''
        .. note:: This method uses the XNAT StoreXML command line utility. This method may be reimplemented in favor of a more native solution.
        
        Store XML into XNAT

        :param filename: XML file name
        :type filename: str
        :raises: :class:`~neuro.exceptions.IOException`
                 :class:`~neuro.command.CommandFailedException`
        '''
        if(not isinstance(filename, basestring)):
            raise IllegalArgumentException("Filename must be an instance of str")

        filename = filesystem.canonical(filename)

        if(not filesystem.isReadable(filename)):
            raise IOException(IOException.READ, filename)

        system.check("StoreXML", "XNAT Utilities")
        command = Command("StoreXML -u " + self._username + " -p " + self._password +
            " -host " + self._url + " -location " + filename + " -allowDataDeletion true")
        command.execute(command)

    def storeXAR(self, filename):
        '''
        .. note:: This method uses the XNAT StoreXAR command line utility. This method may be reimplemented in favor of a more native solution.

        Store XAR into XNAT

        :param filename: 
        :type filename: str
        :raises: :class:`~neuro.exceptions.IOException`
                 :class:`~neuro.command.CommandFailedException`
        '''
        if(not isinstance(filename, basestring)):
            raise IllegalArgumentException("Filename must be an instance of str")

        filename = filesystem.canonical(filename)

        if(not filesystem.isReadable(filename)):
            raise IOException(IOException.READ, filename)

        system.check("StoreXAR", "XNAT Utilities")
        command = Command("StoreXAR -u " + self._username + " -p " + self._password +
            " -host " + self._url + " -f " + filename + " -allowDataDeletion true")
        command.execute(command)

class Xnat_15(Xnat):
    '''
    XNAT 1.5
    '''
    REST_ARCH_PREFIX = '/data/archive/'
    
    def __init__(self, input, timeout=30):
        '''
        Constructor ::
        
            xnat = Xnat("gspcentral")
            xnat = Xnat({"url": "http://gspcentral", "username": "jdoe", "password": "p@$$w0rd"})

        :param input: Alias or dictionary
        :type input: str, dict
        '''
        super(Xnat_15, self).__init__(input, timeout)
    
    def storeXML(self, resource, path=None):
        '''
        .. note:: This method uses the XNAT REST API.
        
        Store XML into XNAT

        :param resource: File name or file-like-object
        :type filename: str
        :raises: :class:`~neuro.exceptions.IOException`
                 :class:`~neuro.command.CommandFailedException`
        '''
        if(isinstance(file, basestring)):
            if(not filesystem.isReadable(filename)):
                raise IOException(IOException.READ, filename)
            filename = filesystem.canonical(filename)
            f = open(filename, 'rb')
        elif(isinstance(resource, file)):
            f = resource
        else:
            raise IllegalArgumentException("Filename must be a string or file-like object")
        
        content = f.read()
        
        ## --- be smart
        if not path:
            root = etree.fromstring(content)
            exp_id = root.xpath('//xnat:imageSession_ID', namespaces={'xnat': root.nsmap['xnat']})
            if exp_id:
                exp_id = exp_id[0].text
                if not exp_id:
                    raise Exception('assessment XML xnat:imageSession_ID is empty')
                deets = self.getMrSessionDetails(aid=exp_id)
                aid = root.get('ID')
                if not aid:
                    raise Exception('Missing ID attribute from your assessment XML')
                path = self.REST_ARCH_PREFIX + 'projects/' + deets['project'] + \
                       '/subjects/' + deets['xnat:mrsessiondata/subject_id'] + \
                       '/experiments/' + exp_id + "/assessors/" + aid
            else:
                raise Exception('missing xnat:imageSession_ID from your assessment XML')
            
        url = self._url + path
        h = httplib2.Http()
        h.add_credentials(self._username, self._password)
        resp, content = h.request(url, "PUT", body=content)
        if resp['status'] not in ['200', '201']:
            response = {'status': int(resp['status']), 'message': content}
            raise RestAPIException(RestAPIException.NOT_OK, url, response)
    
    def getMrSessionDetails(self, aid=None, label=None):
        '''
        Get MR Session details. Passing an Accession ID results in a single dict, passing a label results in a list of dicts.
        
        :param id: MR Session Accession ID
        :param label: MR Session label
        :returns: Dict (aid) or list (label) of MR Session details
        :rtype: list or dict
        '''
        if not aid and not label:
            raise IllegalArgumentException("You must provide either ID or Label")
        elif aid and label:
            raise IllegalArgumentException("You must provide an ID or Label, not both")
        
        id_list = []
        if label:
            id_list = self.getAccessionIDs(label, Xnat.MRSESSION)
        else:
            id_list.append(aid)
        
        results = []
        for id in id_list:
            request = '/experiments?ID=' + id + \
                      '&columns=project,date,label,insert_date,URI,ID,' + \
                      'xnat:mrsessiondata/subject_id&format=json'
            
            result = self.rest_api(request)
            result = json.loads(result)
            
            if 'ResultSet' not in result:
                raise ResultSetException(ResultSetException.NO_RESULT_SET, request=request)
            
            result = result['ResultSet']
            
            if "Result" not in result:
                raise ResultSetException(ResultSetException.NO_RESULT, request=request)
            
            if result["Result"]:
                results.append(result["Result"][0])
                    
        if aid and results:
            return results[0]
        elif aid and not results:
            return {}
        
        return results
    
    def save_assessor_xml(self, xml):
        '''
        Save assessor XML
        
        :param xml: Assessor XML
        :returns: HTTP response code
        :rtype: int
        '''
        pass
    
    def rest_api2(self, path, method=Xnat.GET, query_params={}, params={}):
        '''
        XNAT REST API::
        
            from neuro.apps.xnat import Xnat
            
            xnat = Xnat.create("gspcentral")
            response = xnat.rest_api("/experiments/GSPCentral_S0001_E0001/scans/1/files", query_params={"format": "zip"})
            
        :param path: URL path
        :type path: str
        :param method: Xnat.GET, Xnat.POST, Xnat.PUT, Xnat.DELETE
        :type method: int
        :param query_params: Query parameters
        :type query_params: dict 
        :param params: POST/PUT parameters
        :type params: dict
        :returns: HTTP response object
        :rtype: :class:`~neuro.net.http.HttpResponse`
        '''
        pass
    
    def getAssessorFilesDetails(self, aid, assessor_name, selectors={}):
        '''
        Get Assessment files details given Accession ID
        
        :param aid: Accession ID
        :param name: Assessor name e.g., AB1234C_PerTempAssess1, ExtendedBOLDQC
        :returns: A list of dicts
        :rtype: list
        '''
        if not isinstance(aid, basestring):
            raise IllegalArgumentException("Accession ID must be a string")
        elif not isinstance(assessor_name, basestring):
            raise IllegalArgumentException("Assessor name must be a string")
        elif not isinstance(selectors, dict):
            raise IllegalArgumentException("Selectors must be a dict")
        
        request = "/experiments/" + aid + "/assessors/" + assessor_name + "/files?format=json"
        result = self.rest_api(request)
        result = json.loads(result)

        if("ResultSet" not in result):
            raise ResultSetException(ResultSetException.NO_RESULT_SET, request=request)

        result = result["ResultSet"]

        if("Result" not in result):
            raise ResultSetException(ResultSetException.NO_RESULT, request=request)

        result = result["Result"]
        
        files = []
        for file in result:
            keep = True
            if selectors:
                for k,v in selectors.iteritems():
                    if k not in file or not re.match(v, file[k]):
                        keep = False
                        break
            if(keep):
                files.append(file)
        
        return files
    
    def getAssessorFile(self, aid, assessor_name, selectors):
        '''
        Get an Assessor file from XNAT as a BytesIO object

        :param aid: XNAT Accession ID
        :type aid: str
        :param assessor_name: Assessor name e.g., 'CogAssess1'
        :type assessor_name: str
        :param selectors: Selectors e.g., {'Name': 'foobar.csv'}
        :type selectors: dict
        :returns: BytesIO object
        :rtype: io.BytesIO
        '''            
        file_details = self.getAssessorFilesDetails(aid=aid,
                                                    assessor_name=assessor_name,
                                                    selectors=selectors)

        ## --- only continue if there is one file
        if len(file_details) == 0:
            raise AssessorFileException(AssessorFileException.NO_MATCH, 
                                        aid, assessor_name, selectors, file_details)
        elif len(file_details) > 1:
            raise AssessorFileException(AssessorFileException.MULTI_MATCH, 
                                        aid, assessor_name, selectors, file_details)

        uri = file_details[0].get('URI', None)
        if not uri:
            raise AssessorFileException(AssessorFileException.NO_URI, 
                                        aid, assessor_name, selectors, file_details)

        ## --- read URI contents in BytesIO object
        return BytesIO(self.getPath(uri).strip())
        
    def rest_api(self, path):
        '''
        Request to XNAT Restlet API ::
        
            >>> xnat.rest_api("projects/PROJECT/subjects/SUBJECT/experiments")
            '{"ResultSet":{"Result":[{ ...'
        
        :param path: Restlet URL path
        :type path: str
        :returns: Raw HTTP response body
        :rtype: str
        '''
        if(not isinstance(path, basestring)):
            raise IllegalArgumentException("API path must be an instance of str")

        path = path.lstrip("/")

        if(path.strip() == ""):
            raise IllegalArgumentException("API path cannot be empty")

        request = self.getRequest()
        request = HttpRequest(self.getURL() + "/REST/" + path)
        request.setCredentials(self._username, self._password)
        response = request.send(socktimeout=self._timeout)

        return response.getBody().strip()

    def getScansByCriteria(self, criteria):
        '''
        Get scan details via scan criteria ::
        
            >>> xnat.getScansByCriteria(criteria={"series_description": "fMRI_Soc_Movies"}) 
            '{u'GSPCentral_E00001': [ ...'
          
        :param criteria: 
        :type criteria: dict
        :returns: {session_id: details}
        :rtype: dict
        '''
        isd_attrs = self.getStringAttributes("imageScanData")
        
        path =  "experiments?xsiType=xnat:mrSessionData&" + \
                "columns=xnat:mrSessionData/ID," + \
                "URI," + \
                "xnat:mrSessionData/label"
                
        for attr in isd_attrs:
            path += "," + "xnat:imageScanData/" + attr
                    
        result = json.loads(self.rest_api(path))
        scan_list = result["ResultSet"]["Result"]

        result = {}
        for item in scan_list:
            keep = True
            for key,value in criteria.items():
                if(item["xnat:imagescandata/" + key] != value):
                    keep = False
                    break
            if(keep):
                session_id = item["xnat:mrsessiondata/id"] 
                if(session_id not in result):
                    result[session_id] = []
                result[session_id].append(item)
        
        return result      
    
    def getSavedSearchID(self, name=""):
        '''
        Get saved search ID given saved search name
        
            >>> xnat.getSavedSearchID("overview")
            u'xs1307566322568'
        
        :param name: Name of saved search i.e., brief_description
        :type name: str
        :returns: Saved search ID
        :rtype: str
        '''
        response = self.rest_api("/search/saved?format=json")

        search_ids = json.loads(response)
        for item in search_ids["ResultSet"]["Result"]:
            if(item["brief_description"].lower() == name.lower()):
                return item["id"]
        return None
    
    def getSavedSearchInfo(self, id=None, name=None):
        '''
        Get saved search definition given a saved search name or ID
        
            >>> xnat.getSavedSearchInfo(name="overview")
            <Element {http://nrg.wustl.edu/security}bundle at 0x57d9e10>
            
        :param id: Saved search ID
        :type id: str
        :param name: Saved search name
        :type name: str
        :rtype: lxml.Element
        '''
        if(not name and not id):
            raise IllegalArgumentException("You must provide a name or an id")
        if(name and id):
            raise IllegalArgumentException("You must provide a name _or_ an id")
        if(name):
            id = self.getSavedSearchID(name)
        
        response = self.rest_api("/search/saved/" + id)
        xml = XML(response)
        return xml.toXML()
            
    def getSavedSearch(self, id=None, name=None, format="csv"):
        '''
        Get saved search results. Similar to :class:`getOverview()`
        
            >>> xnat.getSavedSearch(name="overview")
        
        :param id: 
        :type id: str
        :param format: "csv", "json", or "xml"
        :type format: str
        :rtype: csv.reader, json, lxml.etree
        '''
        if(not name and not id):
            raise IllegalArgumentException("You must provide a name or an id")
        if(name and id):
            raise IllegalArgumentException("You must provide a name _or_ an id")
        if(name):
            id = self.getSavedSearchID(name)
                
        result = self.rest_api("/search/saved/" + id + "/results?format=" + format)
        
        if(format == "csv"):
            h = StringIO.StringIO(result)
            return csv.reader(h, delimiter=",")
        elif(format == "json"):
            return json.loads(result)
        elif(format == "xml"):
            xml = XML(result)
            return xml.toXML()
        else:
            raise IllegalArgumentException("XNAT does not support saved search format=" + format)
        
    def getOverview(self, format="csv", version=""):
        '''
        Get XNAT overview table in a variety of formats ::
        
            >>> xnat.getOverview()
            >>> xnat.getOverview("dict")
            >>> xnat.getOverview("dict", "restcorr")

        :param format: Output format "xml", "list", "dict", "array", "csv" or "database"
        :type format: str
        :param version: Overview table "overview", "restcorr", "morphometrics", etc.
        :type version: str
        :rtype: str, list, dict, :class:`~neuro.xml.XML`
        '''
        if(not isinstance(format, basestring)):
            raise IllegalArgumentException("Overview format must be an instance of str")
        elif(not isinstance(version, basestring)):
            raise IllegalArgumentException("Overview version must be an instance of str")

        format = format.strip().lower()
        version = version.strip().lower()

        if(format == ""):
            raise IllegalArgumentException("Overview format cannot be empty")

        ## --- yeah... I suck
        if(version == ""):
            version = "overview"
        elif(version == "morph"):
            version = "morphometrics"

        ## --- GET saved search
        self.debug("Getting saved search from XNAT 1.5")
        search_id = self.getSavedSearchID(version)
        content = self.getSavedSearch(search_id, format="json")
        self.debug("done")
                
        if('ResultSet' not in content):
            raise OverviewException(OverviewException.INVALID, self, version)
        
        content = content["ResultSet"]
        
        if('Columns' not in content):
            raise OverviewException(OverviewException.INVALID, self, version)
        if('Result' not in content):
            raise OverviewException(OverviewException.INVALID, self, version)
        
        columns = content['Columns']
        rows = content['Result']
        
        keep = {}
        for item in columns:
            if('header' in item):
                for k,v in item.iteritems():
                    keep[item["key"]] = item["header"]

        csv_str = StringIO.StringIO()
        writer = csv.writer(csv_str, delimiter=",", quoting=csv.QUOTE_MINIMAL)
        
        writer.writerow(keep.values())
        
        for row in rows:
            csv_row = []
            for key in keep.keys():
                csv_row.append(row[key])
                
            writer.writerow(csv_row)
        
        csv_str.seek(0)
        csv_str = csv_str.read()

        if(format == "csv"):
            return csv_str
        elif(format == "list"):
            return Xnat_15.overviewToList(csv_str)
        elif(format == "xml"):
            return Xnat_15.overviewToXML(csv_str)
        elif(format == "dict"):
            return Xnat_15.overviewToDict(csv_str)
        elif(format == "array"):
            return Xnat_15.overviewToArray(csv_str)
        elif(format == "database"):
            return Xnat_15.overviewToDatabase(csv_str)
        else:
            raise IllegalArgumentException("Overview format must be \"xml\", \"list\", \"dict\", \"array\", or \"csv\"")

    def createArcGet(self):
        '''
        Creates and returns an ArcGet object for the current configuration ::
        
            >>> xnat.createArcGet()
            <neuro.apps.xnat.ArcGetRest object at 0x26677d0>
            
        :rtype: :class:`ArcGet`
        '''
        arcget = ArcGet.create("rest")

        arcget.setUsername(self._username)
        arcget.setPassword(self._password)
        arcget.setURL(self._url)

        if (hasattr(self, "_timeout")):
            arcget.setTimeout(self._timeout)

        return arcget

class Xnat_14(Xnat):
    '''
    XNAT 1.4
    '''
    _overviewPath = "/app/action/SearchAction/querytype/new/element/xnat:mrSessionData/display/overview"
    _morphPath = "/app/action/SearchAction/querytype/new/element/ov:parcov/display/listing"

    def __init__(self, input, timeout=30):
        '''
        Constructor ::
        
            xnat = Xnat("gspcentral")
            xnat = Xnat({"url": "http://gspcentral", "username": "jdoe", "password": "p@$$w0rd"})
        
        :param input: Configuration alias or dict
        :type input: str, dict
        '''
        super(Xnat_14, self).__init__(input, timeout)
    
    def getXMLSearchResult(self, searchElement, searchField, searchValue):
        '''
        Search xnat for something and return the XML string result

        :param searchElement: Element to search e.g. 'xnat:subjectData'
        :type searchElement: str
        :param searchField: Field to search on e.g. 'xnat:subjectData.ID'
        :type searchField: str
        :param searchValue: Value to return e.g. 'Proj_1'
        :type searchValue: str
        :rtype: str
        '''
        if(not isinstance(searchElement, basestring)):
            raise IllegalArgumentException("searchElement must be an instance of str")
        if(not isinstance(searchField, basestring)):
            raise IllegalArgumentException("searchField must be an instance of str")
        if(not isinstance(searchValue, basestring)):
            raise IllegalArgumentException("searchValue must be an instance of str")

        return self.getPath("/app/action/XDATActionRouter/xdataction/xml" +
                    "/search_element/" + searchElement +
                    "/search_field/" + searchField +
                    "/search_value/" + searchValue)

    def getSubjectXMLByLabel(self, label):
        '''
        Get Subject XML by label

        :param label: Subject label
        :type label: str
        :rtype: str
        '''
        if(not isinstance(label, basestring)):
            raise IllegalArgumentException("label must be an instance of str")
        
        return self.getXMLSearchResult('xnat:subjectData','xnat:subjectData.label',label)

    def getSubjectXMLByID(self, id):
        '''
        Get Subject XML by Subject ID

        :param id:
        :type id: str
        :rtype: str
        '''
        if(not isinstance(id, basestring)):
            raise IllegalArgumentException("id must be an instance of str")
        
        return self.getXMLSearchResult('xnat:subjectData','xnat:subjectData.ID',id)

    def getProjectXMLByID(self, id):
        '''
        Get Project XML by ID

        :param id:
        :type id: str
        :rtype: str
        '''
        if(not isinstance(id, basestring)):
            raise IllegalArgumentException("id must be an instance of str")

        return self.getXMLSearchResult('xnat:projectData','xnat:projectData.ID',id)

    def getProjectXMLByName(self, name):
        '''
        Get Project XML by name

        :param name:
        :type name: str
        :rtype: str
        '''
        if(not isinstance(name, basestring)):
            raise IllegalArgumentException("name must be an instance of str")

        return self.getXMLSearchResult('xnat:projectData','xnat:projectData.name',name)

    def getDataTypeCSV(self, dataType):
        '''
        Get datatype details as CSV

        :param dataType: Datatype name e.g. 'xnat:projectData' or 'xnat:subjectData'
        :type dataType: str
        :rtype: str
        '''
        if(not isinstance(dataType, basestring)):
            raise IllegalArgumentException("dataType must be an instance of str")

        ## --- first get the results
        self.getPath("/app/action/DisplaySearchAction?ELEMENT_0=" + dataType)

        ## --- then get the CSV
        return self.getPath("/app/action/CSVAction?ELEMENT_0=" + dataType)

    def getProjectCSV(self):
        '''
        Get Project details as CSV

        :rtype: str
        '''
        return self.getDataTypeCSV("xnat:projectData")

    def getProjectIDList(self):
        '''
        Get list of XNAT Project IDs

        :rtype: list
        '''
        projectIds = []
        array = strings.toArray(self.getProjectCSV())
        array.pop(0)

        for row in array:
            projectIds.append(row[0])
        
        return projectIds

    def getSubjectCSV(self):
        '''
        Get Subject details as CSV

        :rtype: str
        '''
        return self.getDataTypeCSV("xnat:subjectData")

    def getSubjectLabelList(self):
        '''
        Get list of Subject labels

        :rtype: list
        '''
        subjectIds = []
        csv = self.getSubjectCSV()

        array = strings.toArray(csv)
        array.pop(0)

        for row in array:
            subjectIds.append(row[0])

        return subjectIds

    def getMrSessionCSV(self):
        '''
        Get MRSession details as CSV

        :rtype: str
        '''
        return self.getDataTypeCSV("xnat:mrSessionData")

    def getMrSessionLabelList(self):
        '''
        Get list of MRSession labels

        :rtype: list
        '''
        mrSessionIds = []
        array = strings.toArray(self.getMrSessionCSV())
        array.pop(0)

        for row in array:
            mrSessionIds.append(row[0])

        return mrSessionIds

    def getOverview(self, format="csv", version=""):
        '''
        Get XNAT overview table ::
        
            xnat.getOverview()
            xnat.getOverview("dict")
            xnat.getOverview("dict", "restcorr")
        
        :param format: Output format "xml", "list", "dict", "array", or "csv"
        :type format: str
        :param version: Overview table "overview", "restcorr", "morph", etc.
        :type version: str
        :rtype: str, list, dict, :class:`~neuro.xml.XML`
        :raises: :class:`OverviewException`
        '''
        if(not isinstance(format, basestring)):
            raise IllegalArgumentException("Overview format must be an instance of str")
        elif(not isinstance(version, basestring)):
            raise IllegalArgumentException("Overview version must be an instance of str")

        format = format.strip().lower()
        version = version.strip().lower()

        if(format == ""):
            raise IllegalArgumentException("Overview format cannot be empty")

        if(version == ""):
            version = "overview"

        ## --- in a fair and just world, we should always use the last path... but we don't
        if(version == "overview"):
            path = Xnat._overviewPath
        elif(version == "morph"):
            path = Xnat._morphPath
        else:
            path = "/app/action/SearchAction/querytype/new/element/" + version + ":report/display/listing"

        ## --- this whole process involves a double request
        request = self.getRequest()

        request.setPath(path)
        response = request.send(socktimeout=self._timeout)
        validTable = strings.regex('.*<DIV ID="dataTable">.*', response.getBody());

        if(not validTable):
            raise OverviewException(OverviewException.INVALID, self, version)

        request.setPath("/app/action/CSVAction")
        response = request.send(socktimeout=self._timeout)

        ## --- this should be what we want, the overview table as CSV
        body = response.getBody().strip()
        
        if(strings.regex(".*<html>|<HTML>.*", body)):
            raise OverviewException(OverviewException.INVALID, self, version)
        
        if(format == "csv"):
            return body
        elif(format == "list"):
            return Xnat.overviewToList(body)
        elif(format == "xml"):
            return Xnat.overviewToXML(body)
        elif(format == "dict"):
            return Xnat.overviewToDict(body)
        elif(format == "array"):
            return Xnat.overviewToArray(body)
        else:
            raise IllegalArgumentException("Overview format must be \"xml\", \"list\", \"dict\", \"array\", or \"csv\"")
    
    def createArcGet(self):
        '''
        Creates and returns an ArcGet object for the current configuration ::
        
            >>> xnat.createArcGet()
            <neuro.apps.xnat.ArcGetSoap object at 0x26677d0>
        
        :rtype: :class:`ArcGet`
        '''
        arcget = ArcGet.create("soap")
        
        arcget.setUsername(self._username)
        arcget.setPassword(self._password)
        arcget.setURL(self._url)
        
        return arcget

class ArcGet(Object):
    '''
    ArcGet base class
        
    While it is possible to create and populate an ArcGet instance manually ::
    
            from neuro.apps.xnat import ArcGet
        
            arcget = ArcGet.create("rest")
            arcget.setURL("http://gspcentral.dipr.partners.org")
            arcget.setUsername("jdoe")
            arcget.setPassword("p@$$w0rd")
            ...
        
    you may find it more convenient to let a :class:`Xnat` instance take care 
    of this for you ::
            
            from neuro.apps.xnat import Xnat
            
            xnat = Xnat.create("gspcentral")
            arcget = xnat.createArcGet()
            arcget.setSessionID("AB1234C")
            arcget.addScanNumbers([2, 14, 15])
            arcget.setOutputDirectory("/tmp/dicoms")
            output_directory = arcget.execute()
            
    which will populate the XNAT URL, username and password. At the very least 
    you should use a :class:`XnatConfig` for accessing credentials rather than 
    hard-coding them.
    '''

    def __init__(self):
        self._username = None
        self._password = None
        self._url = None
        self._session = None
        self._scanNumbers = []
        self._zipfile = None
        self._outdir = "./arcget"
        self._timeout = 2000
        
    def setUsername(self, username):
        '''
        Set XNAT username
        
        :param username: XNAT Username
        :type username: str
        '''
        ## --- validate inputs
        if(not isinstance(username, basestring)):
            raise IllegalArgumentException("Username must be an instance of str")
        elif(username == ""):
            raise IllegalArgumentException("Username cannot be null")

        self._username = username

    def setPassword(self, password):
        '''
        Set XNAT password
        
        :param password: XNAT Password
        :type password: str
        '''
        ## --- validate inputs      
        if(not isinstance(password, basestring)):
            raise IllegalArgumentException("Password must be an instance of str")
        elif(password == ""):
            raise IllegalArgumentException("Password cannot be null")

        self._password = password

    def setURL(self, url):
        '''
        Set XNAT URL
        
        :param url: XNAT URL
        :type url: str
        '''
        ## --- validate inputs
        if(not isinstance(url, basestring)):
            raise IllegalArgumentException("URL must be an instance of str")
        elif(url == ""):
            raise IllegalArgumentException("URL cannot be null")

        self._url = url

    def setTimeout(self, timeout):
        '''
        Set request timeout (in seconds)
        
        :param timeout: Request timeout in seconds
        :type timeout: int
        '''
        self._timeout = timeout

    def addScanNumber(self, number):
        '''
        Add a scan number for retrieval
            
        :param number: Add single scan number for download
        :type number: int      
        '''
        if(not isinstance(number, int)):
            raise IllegalArgumentException("Scan number must be an integer")
        elif(number <= 0):
            raise IllegalArgumentException("Scan number must be greater than 0")

        if(number not in self._scanNumbers):
            self._scanNumbers.append(number)

    def addScanNumbers(self, numbers):
        '''
        Add scan numbers
        
        :param numbers: Add multiple scan numbers for download
        :type numbers: list
        '''
        if(not isinstance(numbers, list)):
            raise IllegalArgumentException("Scan numbers must be an instance of list")
        elif(len(numbers) == 0):
            raise IllegalArgumentException("Length of scan numbers list must be greater than zero")

        self._scanNumbers = arrays.union(self._scanNumbers, numbers)
    
    def clearScanNumbers(self):
        '''
        Clear scan numbers scheduled for download
        '''
        self._scanNumbers = []

    def setSessionID(self, id):
        '''
        Set session ID
        
        :param id: MR Session label
        :type id: str
        '''
        if(not isinstance(id, basestring) and not isinstance(id, unicode)):
            raise IllegalArgumentException("Session ID must be an instance of str")
        elif(id == ""):
            raise IllegalArgumentException("Session ID cannot be null")
        
        self._session = id
    
    def getScanNumbers(self):
        '''
        Get scan numbers scheduled for download ::
        
            >>> arcget.getScanNumbers()
            [1, 3, 12]
        
        :returns: Scan numbers scheduled for download
        :rtype: list
        '''
        return self._scanNumbers
        
    def setOutputDirectory(self, uri="./arcget"):
        '''
        Set output directory
        
        :param uri: Output directory
        :type uri: str
        '''
        if(not isinstance(uri, basestring)):
            raise IllegalArgumentException("Output directory must be an instance of str")
        
        uri = uri.strip()
        
        if(uri == ""):
            raise IllegalArgumentException("Output directory cannot be null")
        
        if(uri[-1] != "/"):
            uri += "/"
            
        self._outdir = uri

    @staticmethod
    def create(type="soap"):
        '''
        Create an ArcGet instance, factory method ::
            
            >>> ArcGet.create("rest")
            <neuro.apps.xnat.ArcGetRest object at 0x7f8ae653cb10>

        :param type: Type of ArcGet instance e.g., "soap", "rest"
        :type type: str
        :rtype: :class:`ArcGet`
        '''
        if(not isinstance(type, basestring)):
            raise IllegalArgumentException("ArcGet type must be an instance of str")
        elif(type == ""):
            raise IllegalArgumentException("ArcGet type cannot be null")

        type = type.strip().lower()

        if(type == "soap"):
            arcget = ArcGetSoap()
        elif(type == "rest"):
            arcget = ArcGetRest()
        else:
            raise IllegalArgumentException("ArcGet type must be \"soap\" or \"rest\"")

        return arcget

class ArcGetSoap(ArcGet):
    '''
    ArcGet via XNAT Axis/SOAP API    
    ''' 

    def __init__(self):
        '''
        Constructor ::
        
            >>> xnat = ArcGetSoap()
        '''
        ArcGet.__init__(self)

    def execute(self):
        '''
        Execute ArcGet ::
            
            >>> outdir = arcget.execute()
        
        :returns: Output directory
        :rtype: str
        :raises: :class:`~neuro.command.CommandFailedException`
        '''
        ## --- lets deal with making this better latter
        if(self._scanNumbers == None or len(self._scanNumbers) == 0):
            scans = "'ALL'"
        else:
            scans = arrays.implode(self._scanNumbers, ",")

        ## --- check for the ArcGet binary
        system.check("ArcGet", "XNAT SOAP Tools")
        
        ## --- check for anticipated output ZIP file
        zipfile = self._outdir + "/" + self._session + ".zip"
        filesystem.poke(zipfile)
        self._zipfile  = zipfile
        
        ## --- generate and execute the command
        command = Command("ArcGet -host " + self._url + \
                                " -u " + self._username + \
                                " -p " + self._password + \
                                " -s " + self._session + \
                                " -r " + scans + \
                                " -o " + self._outdir)
        
        command.execute()
        
        self._outdir = self._unpack()
        
        return self._outdir
    
    def _unpack(self):
        '''
        Unpack the downloaded archive
        
        :returns: Output directory
        :rtype: str
        :raises: :class:`~neuro.exceptions.FileNotFoundException`, 
                 :class:`~neuro.exceptions.IOException`, 
                 :class:`ArcGetException`,
                 :class:`~neuro.command.CommandFailedException`
        '''     
        if(not filesystem.isReadable(self._zipfile)):
            raise FileNotFoundException(self._zipfile)
        
        system.check("unzip", "Info-ZIP Linux Utility")
        system.check("gunzip", "GNU/Linux Utility")
        
        ## --- list the contents of the zip file
        listing = "unzip -l " + self._zipfile + " | grep RAW"
        command = Command(listing)
        command.execute()
        
        ## --- die if no RAW directory was found within zip
        if(command.getStdout() == ""):
            raise ArcGetException(ArcGetException.NO_RAWDIR, self)

        command = Command("unzip -j " + self._zipfile + " */RAW/* -d " + self._outdir)
        command.execute()

        files = filesystem.dirList(self._outdir)

        if(len(files) == 0):
            raise ArcGetException(ArcGetException.NO_FILES, self)
        
        command = Command("gunzip " + self._outdir + "/*.gz")
        command.execute()

        ## --- lets be paranoid for a moment
        if(filesystem.exists(self._zipfile) and filesystem.isRegularFile(self._zipfile)):
            filesystem.delete(self._zipfile)
        
        return self._outdir
            
class ArcGetRest(ArcGet):
    '''
    ArcGet via XNAT Restlet/REST API    
    '''

    def __init__(self):
        '''
        Constructor ::
        
            >>> arcget = ArcGetRest()
        '''
        ArcGet.__init__(self)

    def execute(self):
        '''
        Execute ArcGet ::
        
            outdir = arcget.execute()

        :returns: Output directory
        :rtype: str
        :raises: :class:`~neuro.net.http.HttpRequestException`
        '''
        ## --- lets deal with making this better latter
        if(self._scanNumbers == None or len(self._scanNumbers) == 0):
            scans = "'ALL'"
        else:
            scans = arrays.implode(self._scanNumbers, ",")

        if not self._session:
            raise ArcGetException(ArcGetException.NO_SESSION, self)
        
        ## --- get Accession ID for MR Session label
        url = self._url + "/data/experiments?columns=ID&label=" + self._session + "&format=json"

        url = self._url + "/data/experiments?columns=ID&label=" + self._session + "&format=json"
        request = HttpRequest(url)
        request.setCredentials(self._username, self._password)
        response = request.send(socktimeout=self._timeout)

        if(response.getStatus() != 200):
            raise HttpRequestException(HttpRequestException.RESPONSE_STATUS, request)

        obj = json.loads(response.getBody())

        if("ResultSet" not in obj):
            raise Exception("Could not retrieve Accession ID for MR Session label '" + self._session + "': no 'ResultSet' in server response")

        results = obj["ResultSet"]["Result"]

        if(not results):
            raise Exception("Could not retrieve Accession ID for MR Session label '" + self._session + "': no 'ResultSet' in server response")
        elif(len(results) > 1):
            raise Exception("Multiple Accession IDs returned for MR Session label '" + self._session + "'")

        result = results[0]
        if("ID" not in result):
            raise Exception("Could not retrieve Accession ID for MR Session label '" + self._session + "': no 'ID' in server response")

        session_id = result["ID"]

        path = "/data/experiments/" + session_id + "/scans/" + scans + "/files?format=zip"

        request.setPath(str(path))

        response = request.send(socktimeout=self._timeout)

        if(response.getStatus() != 200):
            raise HttpRequestException(HttpRequestException.RESPONSE_STATUS, request)

        h = io.BytesIO(response.getBody())
        archive = zipfile.ZipFile(h)

        if(not os.path.isdir(self._outdir)):
            os.makedirs(self._outdir)

        for member in archive.namelist():
            filename = os.path.basename(member)
            source = io.BytesIO(archive.open(member).read())
            ## --- XNAT may return gzipped DICOM files without a .gz extension
            if not filename.endswith('.nii.gz'):
                try:
                    gz_source = gzip.GzipFile(fileobj=source, mode="rb")
                    gz_source.read()
                    source = gz_source
                except IOError, e:
                    pass

            source.seek(0)

            destination = file(os.path.join(self._outdir, filename), "wb")
            shutil.copyfileobj(source, destination)
            source.close()
            destination.close()

        h.close()

        return self._outdir

class ConfigException(BaseException):
    '''
    Invalid configuration file exception
    '''
    MISSING_DEP=1
    MISSING_OPT=2
    BLANK_OPT=3
    NOT_LOADED=4

    def __init__(self, type, file, deployment, option=""):
        '''
        Constructor

        :param type: ConfigException.MISSING_DEP, ConfigException.MISSING_OPT, ConfigException.BLANK_OPT
        :type type: int
        :param file: Configuration file name
        :type file: str
        :param option: Element name e.g., "deployment", "url", "username"
        :type option: str
        '''
        BaseException.__init__(self)

        if(not isinstance(type, int)):
            raise IllegalArgumentException("Type must be an int")
        elif(not isinstance(file, basestring)):
            raise IllegalArgumentException("File name must be an instance of str")
        elif(not isinstance(deployment, basestring)):
            raise IllegalArgumentException("Deployment must be an instance of str")
        elif(not isinstance(option, basestring)):
            raise IllegalArgumentException("Option must be an instance of str")

        file = file.strip()
        deployment = deployment.strip()
        option = option.strip()

        if(file == ""):
            raise IllegalArgumentException("File cannot be empty")
        elif(deployment == ""):
            raise IllegalArgumentException("Deployment cannot be empty")

        if(type < 1 or type > 4):
            raise IllegalArgumentException("Type must be MISSING_DEP/1, MISSING_OP/2, BLANK_OP/3, NOT_LOADED/4")

        self._type = type
        self._file = file
        self._deployment = deployment
        self._option = option

    def getType(self):
        '''
        Get the exception type

        :rtype: int
        '''
        return self._type

    def getFile(self):
        '''
        Get file that triggered this exception

        :rtype: str
        '''
        return self._file

    def getOption(self):
        '''
        Get option that triggered this exception

        :rtype: str
        '''
        return self._option

    def getMessage(self):
        '''
        Get message summarizing this exception

        :rtype: str
        '''
        if(self._type == ConfigException.MISSING_DEP):
            message = "Missing deployment \"" + self._deployment + "\" in config file: " + self._file
        elif(self._type == ConfigException.MISSING_OPT):
            message = "Missing element \"" + self._option + "\" in deployment \"" + self._deployment + "\" in config file: " + self._file
        elif(self._type == ConfigException.BLANK_OPT):
            message = "Blank element \"" + self._option + "\" in deployment \"" + self._deployment + "\" in config file: " + self._file
        elif(self._type == ConfigException.NOT_LOADED):
            message = "Configuration file not loaded: " + self._file
            
        return message

    def __str__(self):
        '''
        Get string representation of this error

        :rtype: str
        '''
        return self.getMessage()

class OverviewException(BaseException):
    '''
    Overview table exception
    '''
    INVALID=1

    def __init__(self, type, xnat, version):
        '''
        Constructor

        :param type: OverviewException.INVALID
        :type type: int
        '''
        BaseException.__init__(self)

        if(not isinstance(type, int)):
            raise IllegalArgumentException("Exception type must be an instance of int")
        elif(not isinstance(xnat, Xnat)):
            raise IllegalArgumentException("Xnat parameter must be an instance of Xnat")
        elif(not isinstance(version, basestring)):
            raise IllegalArgumentException("Overview table version must be an instance of str")

        if(type != 1):
            raise IllegalArgumentException("Exception type must be OverviewException.INVALID")

        self._type = type
        self._xnat = xnat
        self._version = version

    def getType(self):
        '''
        Get exception type

        :rtype: int
        '''
        return self._type

    def getXnat(self):
        '''
        Get Xnat reference that triggered this exception

        :rtype: Xnat
        '''
        return self._xnat

    def getVersion(self):
        '''
        Get overview version string that triggered this exception

        :rtype: str
        '''
        return self._version

    def getMessage(self):
        '''
        Get custom message

        :rtype: str
        '''
        if(self._type == OverviewException.INVALID):
            return "Queried overview table for version \"" + self._version + "\" appears to be invalid"

class RestAPIException(BaseException):
    '''
    XNAT REST API Exception
    '''
    NOT_OK=0
    
    def __init__(self, type, url, response):
        BaseException.__init__(self)
        
        self._type = type
        self._url = url
        self._response = response
        
    def getMessage(self):
        if self._type == RestAPIException.NOT_OK:
            return "HTTP Status=" + str(self._response['status']) + ", URI=" + self._url + ", Message=" + self._response['message'] 

class AssessorFileException(BaseException):
    '''
    XNAT REST API Exception
    '''
    NO_MATCH=0
    MULTI_MATCH=1
    NO_URI=2
    
    def __init__(self, type, aid, assessor_name, selectors, file_details):
        BaseException.__init__(self)
        
        self._type = type
        self._aid = aid
        self._assessor_name = assessor_name
        self._selectors = selectors
        self._file_details = file_details
        
    def getMessage(self):
        if self._type == AssessorFileException.NO_MATCH:
            return "No Assessor file for " + self._aid + ":" + self._assessor_name + " matching " + str(self._selectors)
        elif self._type == AssessorFileException.MULTI_MATCH:
            return "Multiple files found for " + self._aid + ":" + self._assessor_name + " matching " + str(self._selectors)
        elif self._type == AssessorFileException.NO_URI:
            return "No URI field returned for " + self._aid + ":" + self._assessor_name + " file matching " + str(self._selectors)
        
class XnatApiException(BaseException):
    '''
    XNAT API exception
    '''
    RESPONSE=0
    JSON_ERROR=1
    NO_VALUES=2
    EMPTY_VALUES=3
    BLANK_VALUE=4

    def __init__(self, type, xnat, call, response=""):
        '''
        Constructor

        :param type: XnatApiException.JSON_ERROR, XnatApiException.NO_VALUES, etc.
        :type param: int
        :param xnat: Xnat object
        :type xnat: :class:`Xnat`
        :param call: 
        :type call: str
        :param response:
        :type response: str
        '''
        BaseException.__init__(self)

        if(not isinstance(type, int)):
            raise IllegalArgumentException("Exception type must be an instance of int")
        elif(not isinstance(xnat, Xnat)):
            raise IllegalArgumentException("Xnat parameter must be an instance of Xnat")
        elif(not isinstance(call, basestring)):
            raise IllegalArgumentException("API call must be an instance of str")
        elif(not isinstance(response, basestring)):
            raise IllegalArgumentException("Response must be an instance of str")

        if(type < 0 or type > 3):
            raise IllegalArgumentException("Exception type out of range")

        self._type = type
        self._xnat = xnat
        self._call = call
        self._response = response

    def getType(self):
        '''
        Get exception type

        :rtype: int
        '''
        return self._type

    def getXnat(self):
        '''
        Get Xnat reference that triggered this exception

        :rtype: :class:`Xnat`
        '''
        return self._xnat

    def getResponse(self):
        '''
        Get response that triggered this exception

        :rtype: str
        '''
        return self._response

    def getMessage(self):
        '''
        Get custom message

        :rtype: str
        '''
        if(self._type == XnatApiException.RESPONSE):
            return "Server returned an invalid response for API call: " + self._call
        elif(self._type == XnatApiException.JSON_ERROR):
            obj = json.loads(self._response)
            return "Server response for API call " + self._call + " contained an error: " + obj["error"]
        elif(self._type == XnatApiException.NO_VALUES):
            return "Server response for API call " + self._call + " contained no values array"
        elif(self._type == XnatApiException.EMPTY_VALUES):
            return "Server response for API call " + self._call + " contained an empty values array"
        elif(self._type == XnatApiException.BLANK_VALUE):
            return "Server response for API call " + self._call + " containted a blank value"

class ArcGetException(BaseException):
    '''
    ArcGet exception
    '''
    NO_RAWDIR=0
    NO_FILES=1
    NO_SESSION=2

    def __init__(self, type, arcget):
        '''
        Constructor

        :param type: ArcGetException.NO_RAWDIR, ArcGetException.NO_FILES, etc.
        :type type: int
        :param arcget:
        :type arcget: :class:`ArcGet`
        '''
        BaseException.__init__(self)

        if(not isinstance(type, int)):
            raise IllegalArgumentException("Exception type must be an instance of int")
        elif(not isinstance(arcget, ArcGet)):
            raise IllegalArgumentException("ArcGet parameter must be an instance of ArcGet")

        self._type = type
        self._arcget = arcget

    def getType(self):
        '''
        Get exception type

        :rtype: int
        '''
        return self._type

    def getArcGet(self):
        '''
        Get ArcGet reference that triggered this exception

        :rtype: :class:`ArcGet`
        '''
        return self._arcget

    def getMessage(self):
        '''
        Get custom message

        :rtype: str
        '''
        if(self._type == ArcGetException.NO_RAWDIR):
            return "ArcGet archive does not contain a RAW directory"
        elif(self._type == ArcGetException.NO_FILES):
            return "ArcGet archive does not contain any DICOM files"
        elif(self._type == ArcGetException.NO_SESSION):
            return "ArcGet object has no MR Session ID set"
        
class ResultSetException(BaseException):
    '''
    REST API ResultSet exception
    '''
    NO_RESULT_SET=1
    NO_RESULT=2

    def __init__(self, code, request):
        '''
        Constructor

        :param type: ResultSetException.NO_RESULT_SET, ResultSetException.NO_RESULT, etc.
        :type code: int
        :param request: REST API request
        :type request: str
        '''
        BaseException.__init__(self)

        if(not isinstance(code, int)):
            raise IllegalArgumentException("Exception code must be an instance of int")
        elif(not isinstance(request, basestring)):
            raise IllegalArgumentException("Request must be an instance of str")

        self._code = code
        self._request = request

    def getMessage(self):
        '''
        Get custom message

        :rtype: str
        '''
        if(self._code == ResultSetException.NO_RESULT_SET):
            return "No ResultSet in response for API request: " + self._request
        elif(self._code == ResultSetException.NO_RESULT):
            return "No Result in response for API request: " + self._request
